
/*
 * simulOptions {
 * 		map: google.maps.Map,			// map to animate on
 * 		marker: google.maps.Marker,		// marker to animate
 * 		directions: DirectionResult,	// directions to simulate
 * 		tick: number,					// miliseconds between each step
 * 		trackMarker: Boolean,			// Keep map centered on marker,
 * 		callback: Function,				// finished callback
 * 		speed: number,					// marker speed. 'legal' indicates road recommended speed.
 * 		loop: Boolean					// loop when finished
 * }
 */
function MarkerSimulator(simulOptions) {
	
	this.options = simulOptions;
	this.timer = null;
	
	this.totalDistancePassed = 0;
	this.currStepIndx = 0;
	this.currStepDistancePassed = 0;
	
	this.panCounter = 0;
	this.currSpeed = 0;
			
	this.run = function(){
		this.timer = window.setInterval(this.animateStep, this.options.tick, this); // pass 'this' - simulator reference
	}
	
	this.pause = function() {
	    if (this.timer)
		    clearInterval(this.timer); // disable timer
		this.timer = null;
		this.currSpeed = 0;
	}
	
	this.restart = function() {
		this.totalDistancePassed = 0;
		this.currStepIndx = 0;
		this.currStepDistancePassed = 0;
		
		this.moveMarker(this.options.directions.routes[0].legs[0].start_location);
	}
	
	this.setEnabled = function(isEnabled) {
		if (isEnabled)
			this.run();
		else 
			this.pause();
	}
	
	this.isRunning = function() { 
		return timer != null;
	}
	
	this.moveMarker = function (position) {
		
		this.options.marker.setPosition(position);
		
		this.panCounter++;
		if (this.options.trackMarker && this.panCounter == 10){ 
			this.options.map.panTo(position);
		    this.panCounter = 0;
		}
	}
	
	/*
	 * recieves 'sim' - simulator reference (to fix calling context issues)
	 */
	this.animateStep = function(sim) {
		
		var myRoute = sim.options.directions.routes[0].legs[0];
    
		// check stopping condition - did we arrive to our destination?
		if (sim.totalDistancePassed >= myRoute.distance.value) { 
			if(sim.options.endcallback)
				sim.options.endcallback();
				
			if (sim.options.loop) 
				sim.restart();
			else 
				sim.pause();
			return;
		}
		
		// set speed for this animation step
		var currSpeed = sim.options.speed;
		var step_distance = myRoute.steps[sim.currStepIndx].distance.value; // meters
		var step_duration = myRoute.steps[sim.currStepIndx].duration.value; // seconds
		
		if (currSpeed == "legal")
			currSpeed = step_distance / step_duration; // meter/sec
		else {
			// just convert km/hr --> m/s
			currSpeed = currSpeed / 3.6;
		}
		
		sim.currSpeed = currSpeed * 3.6; // for broadcast
		
		// calculate distance to travel this animation step as minimum of (a full speed step, or whats left of the route)
		var dist2travel = Math.min(currSpeed * sim.options.tick / 1000.0, myRoute.distance.value - sim.totalDistancePassed);
		
		var newPosition;
		
        while (dist2travel > 0) {
        	step_path = myRoute.steps[sim.currStepIndx].path; // Array(LatLng)
        	
			newPosition = GetPointAtDistance(step_path, sim.currStepDistancePassed + dist2travel);
            
            if (newPosition == null) { // step is too short, goto next step
				var leftOver = step_distance - sim.currStepDistancePassed;
				sim.totalDistancePassed += leftOver;
				sim.currStepDistancePassed = 0;
				sim.currStepIndx++;
				dist2travel -= leftOver;
				continue;
            }
            else {
				sim.totalDistancePassed += dist2travel;
				sim.currStepDistancePassed += dist2travel;
				dist2travel = 0;
				break;
            }
        }
        
		sim.moveMarker(newPosition);
		
		if(sim.options.stepcallback)
				sim.options.stepcallback(sim.totalDistancePassed, currSpeed);
				
	}
}

// The following formulas are adapted from the Aviation Formulary
// http://williams.best.vwh.net/avform.htm

var nauticalMilePerLat = 60.00721;
var nauticalMilePerLongitude = 60.10793;
var rad = Math.PI / 180.0;
var milesPerNauticalMile = 1.15078;
var kmsPerMile = 1.609344;
var kmsPerNauticalMile = milesPerNauticalMile * kmsPerMile;

//Caclulate distance (in meters) between two lat lons in NM
function calcDistance(geoPt1, geoPt2){
    yDistance = (geoPt2.lat() - geoPt1.lat()) * nauticalMilePerLat;
    xDistance = (Math.cos(geoPt1.lat() * rad) + Math.cos(geoPt2.lat() * rad)) * (geoPt2.lng() - geoPt1.lng()) * (nauticalMilePerLongitude / 2);
    distance = Math.sqrt(yDistance * yDistance + xDistance * xDistance);
    
    return distance * kmsPerNauticalMile * 1000;
}


// === A method which returns a GLatLng of a point a given distance along the path ===
// === Returns null if the path is shorter than the specified distance ===
function GetPointAtDistance(path, meters){
    // some awkward special cases
    if (meters == 0) 
        return path[0];
    if (meters < 0) 
        return null;
    
    var dist = 0;
    var olddist = 0;
    for (var i = 1; (i < path.length && dist < meters); i++) {
        olddist = dist;
        dist += calcDistance(path[i], path[i - 1]);
    }
    if (dist < meters) 
        return null;
    
    var p1 = path[i - 2];
    var p2 = path[i - 1];
    var m = (meters - olddist) / (dist - olddist);
    return new google.maps.LatLng(p1.lat() + (p2.lat() - p1.lat()) * m, p1.lng() + (p2.lng() - p1.lng()) * m);
}
